<?php
/**
 * @package au2 
 * @author Jaroslaw Mezyk <j.mezyk@netcoffee.pl>
 * @copyright Jaroslaw Mezyk 2008
 * @license GPL 3
 */

require_once dirname(__FILE__) . '/variables_au2.class.php';

/**
 * Handles all template file processing
 *
 * @package au2
 * @license GPL 3
 * @author Jaroslaw Mezyk <j.mezyk@netcoffee.pl>
 * @copyright Jaroslaw Mezyk 2008
 */
class template_au2{

	const MOD_IGNORE_TRIGGERS = 1;

	const MOD_INCLUDED = MOD_IGNORE_TRIGGERS;

	private $constModifiers = 0;


	/**
	 * path to template file
	 * 
	 * @var string
	 * @access private
	 */
	private $sourceFileName = ''; 


	/**
	 * path to target file
	 * 
	 * @var string
	 * @access private
	 */
	private $targetFileName = '';


	/**
	 * keeps unmodified source
	 * 
	 * @var string
	 * @access private
	 */
	private $originalSource = '';

	/**
	 * keeps unmodified body (source without headers)
	 * 
	 * @var string
	 * @access private
	 */
	private $originalBody = '';

	/**
	 * keeps current source (modified)
	 * 
	 * @var string
	 * @access private
	 */
	private $contents = '';


	/**
	 * keeps new line characters of the template file
	 * 
	 * @var string
	 * @access private
	 * @see openSourceFile()
	 */
	private $newLineCharacter = "\r\n";


	/**
	 * Keeps reference to global singleton object - variables_au2
	 * 
	 * @var variables_au2
	 * @access private
	 */
	private $vars;


	/**
	 * Keeps headers assigned to template. 
	 *
	 * Array of hashes. Each hash contains one header:
	 * <pre>
	 * 	array('name' => 'header name', 'value' => 'header value');
	 * </pre>
	 * 
	 * @var array
	 * @access private
	 * @see addHeader()
	 * @see getNextHeader()
	 */
	private $haHeaders = array();



	/**
	 * Keeps index of headers, so it is easy  to find position of headers by name
	 *
	 * hash of arrays. Every array contains indexes of headers with given name in
	 * haHeaders.
	 * egzample:
	 * if $this -> haHeaders is:
	 * <pre>
	 * array(
	 * 		array('name' => 'var', 'value' => 'a=8') // index: 0
	 * 	,	array('name' => 'var', 'value' => 'b=3') // index: 1
	 * 	,	array('name' => 'var', 'value' => 'c=9') // index: 2
	 * 	,	array('name' => 'target', 'value' => 'main.css') // index: 3
	 * )
	 * </pre>
	 * then $this -> ahHeadersIndex  is:
	 * <pre>
	 * array(
	 * 		'var' => array(0, 1, 2)
	 * 	,	'target' => array(3)
	 * )
	 * </pre>
	 * 
	 * @var array
	 * @access private
	 * @see addHeader()
	 * @see getNextHeaderByName()
	 */
	private $ahHeadersIndex = array();

	/**
	 * constructor
	 * 
	 * @param string $sFileName path to template file
	 * @access public
	 * @return void
	 * @throws Exception - if file can not be found or is not readable
	 */
	function __construct($sFileName, $constModifiers = 0){
/*{{{*/
		if(file_exists($sFileName) and is_file($sFileName) and is_readable($sFileName)){
			$this -> constModifiers = $constModifiers;

			$this -> vars = variables_au2::singleton();

			$this -> sourceFileName = $sFileName;
			$this -> openSourceFile();

			$this -> saveTargetFile();


		}else{
			throw new Exception("Template file '{$sFileName}' not found or can not be read");
		}
/*}}}*/
	}

	/**
	 * reads source file and makes all processing
	 * 
	 * @access private
	 * @return void
	 */
	private function openSourceFile(){
/*{{{*/

		$sSource = file_get_contents($this -> sourceFileName);
		$this -> originalSource = $sSource;

		/**
		 * New line character detection  
		 * If file contains \r\n 
		 */
		if(strpos($sSource, "\r\n") !== false){
			// leave default newLineCharacter (windows style)
		}elseif(strpos($sSource, "\r") !== false){
			// (Apple style)
			$this -> newLineCharacter = "\r";
		}elseif(strpos($sSource, "\n") !== false){
			// Unix style
			$this -> newLineCharacter = "\n";
		}else{
			// no new line characters found. Leaving default value
		}





		list($sHeaders, $sSource) = explode($this -> newLineCharacter . $this -> newLineCharacter, $sSource, 2);

		$sHeaders = str_replace($sTemplateHeadersMarker, '', $sHeaders);

		$sHeaders = trim($sHeaders);
		$sSource = trim($sSource);

		$this -> contents = $sSource;
		$this -> originalBody = $sSource;

		/**
		 * reading template headers 
		 */
		$saHeaders = explode($this -> newLineCharacter, $sHeaders);
		$iCnt = sizeof($saHeaders);
		for($x = 0; $x < $iCnt; $x++){
			list($sHeaderName, $sHeaderValue) = explode(':', $saHeaders[$x], 2);
			$sHeaderName = trim($sHeaderName);
			$sHeaderValue = trim($sHeaderValue);

			/**
			 * shorthand method of setting variable
			 * insetad of: 
			 * 	var: variablename = variablevalue
			 * one can write:
			 * 	variablename = variablevalue
			 *
			 * requirements:
			 * 	- header name must contain =
			 * 	- header value must be empty
			 * 	- header line must not contain :
			 */
			if($sHeaderValue == '' and strpos($sHeaderName, '=') !== false and strpos($saHeaders[$x], ':') === false){
				$sHeaderValue = $sHeaderName;
				$sHeaderName = 'var';
			}

			$this -> addHeader($sHeaderName, $sHeaderValue);
		}


		/**
		 * Do all processing stuff  
		 */
		$this -> processTemplate();

/*}}}*/
	}


	/**
	 * adds header value to headers registry, and performs action specific to header
	 *
	 * Adds header to $haHeaders and $ahHeadersIndex
	 * Performs action specific to header
	 * 
	 * @param string $sHeaderName name of the header
	 * @param string $sHeaderValue value of the header
	 * @access public
	 * @return void
	 * @see haHeaders
	 * @see ahHeadersIndex
	 */
	public function addHeader($sHeaderName, $sHeaderValue){
/*{{{*/
		// add header to $this -> haHeaders and get it's index
		$iHeaderIndex = array_push($this -> haHeaders, array('name' => $sHeaderName, 'value' => $sHeaderValue)) - 1;

		// create array of indexes for given header name if not exists
		if(!isset($this -> ahHeadersIndex[$sHeaderName])){
			$this -> ahHeadersIndex[$sHeaderName] = array();
		}

		// add header's index to index
		$this -> ahHeadersIndex[$sHeaderName][] = $iHeaderIndex;


		/**
		 * Setting variable value using header 
		 * Header name is 'var' and header value is: variable_name=variable_value.
		 * We are using $this -> vars to manage all the variables
		 */
		if($sHeaderName == 'var'){
/*{{{*/
			list($sVariableName, $sVariableValue) = explode('=', $sHeaderValue, 2);
			$sVariableName = trim($sVariableName);
			$sVariableValue = trim($sVariableValue);

			$this -> vars -> setVar($sVariableName, $sVariableValue);
/*}}}*/
		}


		/**
		 * Setting output file  
		 */
		if($sHeaderName == 'output'){
/*{{{*/
			try{
				$this -> setTargetFile($sHeaderValue);
			}catch(Exception $e){
				// can not set output file
			}
/*}}}*/
		}

		/**
		 * Use template 
		 */
		if($sHeaderName == 'template'){
			$this -> useTemplate($sHeaderValue);
		}

		/**
		 * Import variables from outer file 
		 */
		if($sHeaderName == 'import-var'){
			$this -> importVariables($sHeaderValue);
		}
		

		/**
		 * Processing other files
		 */
		if($sHeaderName == 'trigger' and !($this -> constModifiers & MOD_IGNORE_TRIGGERS)){
/*{{{*/
			try{
				$oSzablon = new template_au2($this -> expandPath($sHeaderValue));

			}catch(Exception $e){
			}
/*}}}*/
		}
/*}}}*/
	}


	/**
	 * sets target file
	 * 
	 * @param string $sFile path to target file
	 * @access private
	 * @return void
	 * @throws Exception if file is not writable
	 */
	private function setTargetFile($sFile){
/*{{{*/
		$sFile = $this -> expandPath($sFile);

		// check if path is directory path
		$bDirectory = (in_array(substr($sFile, -1), array('/', "\\")));


		if($bDirectory){
			$this -> setTargetFile($sFile . basename($this -> sourceFileName));

		}else{
			if(file_exists($sFile)){
				// file already exists
				if(is_writable($sFile)){
					$this -> targetFileName = $sFile;
				}else{
					throw new Exception("Target file '{$sFile}' is not writable");
				}
			}else{
				// file does not exist
				if(is_writable(dirname($sFile))){
					$this -> targetFileName = $sFile;
				}else{
					$sDir = dirname($sFile);
					throw new Exception("Target directory '{$sDir}' is not writable");
				}
			}

		}
/*}}}*/
	}



	/**
	 * Does all the processing stuff on template
	 * 
	 * @access private
	 * @return void
	 */
	private function processTemplate(){
/*{{{*/


		/**
		 * @todo Commands below should be processed as long as any of them 
		 * returns TRUE (which means that contents has been changed)
		 */
		$this -> expandVariables();
		$this -> includeFiles();
		$this -> processTGetters();




		/**
		 * Commands below should be called only once (after processing) 
		 */

		$this -> performAfterProcessActions();

/*}}}*/
	}



	/**
	 * Replaces all variable placeholders with variable values
	 * 
	 * Variable placeholder is in form of: [var::variable_name]
	 * Variable names are case sensitive. Variable may consists a-Z letters, 0-9 digits and underscore _
	 * Variable name starts with letter or underscore
	 *
	 * @access private
	 * @return boolean retuns true if any replacement was made or false if source is unchanged
	 */
	private function expandVariables(){
		/*{{{*/
		$sTextGetterRE = '!\[getText::(?P<syntax>[a-zA-Z_][a-zA-Z0-9_-]*),(?P<hook>[^\]\[]*)\]!msuSU';

		preg_match_all($sTextGetterRE, $this -> contents, $shFound);

		$iCnt = sizeof($shFound['syntax']);
		if($iCnt > 0){

			for($x = 0; $x < $iCnt; $x++){

				$sSyntaxName = $shFound['syntax'][$x];
				$sHook = $shFound['hook'][$x];

				try{
					$oPlugin = $this -> getPlugin('get-text', $sSyntaxName);
					$oPlugin -> setContents($this -> originalBody);
					$sReplace = $oPlugin -> getText($sHook);

				}catch(Exception $e){
					$sReplace = '';
				}

				$this -> contents = str_replace($shFound[0][$x], $sReplace, $this -> contents);
			}

		}else{
			return false;
		}

		/*}}}*/
	}

	/**
	 * Handles all [include::filename] and [include-au2::filename] directives
	 * 
	 * @access private
	 * @return boolean retuns true if any replacement was made or false if source is unchanged
	 */
	private function includeFiles(){
		/*{{{*/
		$sIncludeRE = '!\[include(?P<au>-au2)?::(?P<filename>[a-zA-Z_/\\0-9.-]+)\]!msuSU';

		preg_match_all($sIncludeRE, $this -> contents, $shFound);

		$iCnt = sizeof($shFound['filename']);
		if($iCnt > 0){

			for($x = 0; $x < $iCnt; $x++){
				$sFileName = $shFound['filename'][$x];
				$sFilePath = $this -> expandPath($sFileName);

				if($shFound['au'][$x] == '-au2'){
					try{
						$oSzablon = new template_au2($sFilePath, MOD_INCLUDED);
						$sIncludedFileContents = $oSzablon -> getContents();

					}catch(Exception $e){
						$sIncludedFileContents = '';
					}
					$this -> contents = str_replace("[include-au2::{$sFileName}]", $sIncludedFileContents, $this -> contents);
				}else{
					$sIncludedFileContents = $this -> getFileContents($sFilePath);
					$this -> contents = str_replace("[include::{$sFileName}]", $sIncludedFileContents, $this -> contents);
				}
			}

		}else{
			return false;
		}

		/*}}}*/
	}


	private function processTGetters(){



		$sVariableRE = '!\[var::(?P<variable>[a-zA-Z_][a-zA-Z0-9_]*)(?:,(?P<defaultValue>[^\]\[]*))?\]!msuSU';

		preg_match_all($sVariableRE, $this -> contents, $shFound);

		$iCnt = sizeof($shFound['variable']);
		if($iCnt > 0){

			for($x = 0; $x < $iCnt; $x++){

				$sVariableName = $shFound['variable'][$x];
				$sVariableValue = $this -> vars -> getVar($sVariableName, $shFound['defaultValue'][$x]);

				$this -> contents = str_replace($shFound[0][$x], $sVariableValue, $this -> contents);
			}

		}else{
			return false;
		}




	}





	/**
	 * Returns processed contents
	 * 
	 * @access public
	 * @return string
	 */
	public function getContents(){
/*{{{*/
		return $this -> contents;
/*}}}*/
	}


	/**
	 * Saves processed contents to target file
	 * 
	 * @access private
	 * @return void
	 */
	private function saveTargetFile(){
/*{{{*/
		if($this -> targetFileName != ''){
			file_put_contents($this -> targetFileName, $this -> contents);
		}
/*}}}*/
	}



	/**
	 * returns path to file, relative to template path
	 * or unchanged path if path is absolute
	 * 
	 * @param string $sPath path
	 * @access private
	 * @return string
	 */
	private function expandPath($sPath){
/*{{{*/
		// check if path is absolute
		$bAbsolute = ($sPath[1] == ':' or $sPath[0] == '/');

		if(!$bAbsolute){
			$sPath = dirname($this -> sourceFileName) . '/' . $sPath;
		}

		return $sPath;
/*}}}*/
	}



	/**
	 * Expands given path and returns file contents
	 * 
	 * @param string $sFile 
	 * @access private
	 * @return string 
	 * @throws Exception if file does not exist or is not readable
	 */
	private function getFileContents($sFile){
/*{{{*/
		$sOFile = $sFile;
		$sFile = $this -> expandPath($sFile);

		if(is_file($sFile) and is_readable($sFile)){
			return file_get_contents($sFile);
		}else{
			throw new Exception("File '{$sOFile}' that expands to '{$sFile}' does not exist or is not readable.");
		}
/*}}}*/
	}


	/**
	 * Replaces contents of current template ($this -> contents)
	 * with contents of template file. Original source ($this -> originalSource)
	 * remains unchanged.
	 * 
	 * @param string $sTemplateFile Path to template file
	 * @access private
	 * @return void
	 * @see contents
	 */
	private function useTemplate($sTemplateFile){
/*{{{*/
		try{
			
			$sTemplate = $this -> getFileContents($sTemplateFile);
			$this -> contents = $sTemplate;
			$this -> contents = str_replace('[tpl::originalContents]', $this -> originalBody, $this -> contents);

		}catch(Exception $e){
		}
/*}}}*/
	}



	/**
	 * Calls import() method on $this -> vars object
	 * that imports variables from outer file
	 * 
	 * @param string $sFile Variables file
	 * @access private
	 * @return void
	 * @throws Exception if import fails
	 */
	private function importVariables($sFile){
		/*{{{*/
		$sFile = $this -> expandPath($sFile);

		try{
			$this -> vars -> import($sFile);

		}catch(Exception $e){
			throw new Exception("File '{$sFile}' could not be imported");
		}
/*}}}*/
	}


	/**
	 * Does all the stuff that should be done after template is processed
	 * (e.g. calling after-process modules
	 * 
	 * @access private
	 * @return void
	 */
	private function performAfterProcessActions(){
/*{{{*/

		/**
		 * Run all after-proces haders
		 */

		$this -> resetHeaderByName('after-process');

		while($sModule = $this -> getNextHeaderByName('after-process')){

			try{
				$oPlugin = $this -> getPlugin('content-modify', $sModule);

				$oPlugin -> setContents($this -> contents);
				$oPlugin -> run();
				$this -> contents = $oPlugin -> getContents();

			}catch(Exception $e){


			}


		}
/*}}}*/
	}



	/**
	 * Resets pointer on headers index table
	 * 
	 * @param string $sHeaderName header name
	 * @access private
	 * @return boolean
	 */
	private function resetHeaderByName($sHeaderName){
/*{{{*/
		if(is_array($this -> ahHeadersIndex[$sHeaderName])){
			return reset($this -> ahHeadersIndex[$sHeaderName]);
		}else{
			return false;
		}
/*}}}*/
	}


	/**
	 * Returns value of next header with given name or false if 
	 * there is no more headers with given name
	 *
	 * use resetHeaderByName() before calling getNextHeaderByName in loop
	 * 
	 * @param string $sHeaderName name of header
	 * @access private
	 * @return mixed
	 * @see addHeader()
	 * @see resetHeaderByName()
	 */
	private function getNextHeaderByName($sHeaderName){
	/*{{{*/
		if(is_array($this -> ahHeadersIndex[$sHeaderName])){
			$iId = current($this -> ahHeadersIndex[$sHeaderName]);

			next($this -> ahHeadersIndex[$sHeaderName]);
			
			if($iId){
				return $this -> haHeaders[$iId]['value'];
			}else{
				return false;
			}
		}else{
			return false;
		}
/*}}}*/
	}




	/**
	 * creates new instance of plugin class and returns it
	 * 
	 * @param string $sPluginType - type of the plugin 
	 * @param string $sPluginName - name of the plugin
	 * @access private
	 * @return object
	 * @throws Exception if sth goes wrong
	 */
	private function getPlugin($sPluginType, $sPluginName){
/*{{{*/
		/**
		 * Check if base class exists (check file, require it once and check if class exists)
		 */
		$sBaseClassName = 'au2_plugin__' . str_replace('-', '_', $sPluginType);
		$sBaseClassFile = dirname(__FILE__) . '/' . $sBaseClassName . '.class.php';

		if(file_exists($sBaseClassFile)){
			require_once $sBaseClassFile;

			if(!class_exists($sBaseClassName)){
				throw new Exception('Plugin base class does not exist');
			}

		}else{
			throw new Exception('Plugin base class file does not exist');
		}

		/**
		 * Check if plugin file exists
		 */

		$sPluginClassName = str_replace('-', '_', $sPluginName);
		$sPluginFile = dirname(__FILE__) . '/plugins/' . str_replace('-', '_', $sPluginType) . '/' . $sPluginClassName . '.class.php';

		if(file_exists($sPluginFile)){
			require_once $sPluginFile;

			if(class_exists($sPluginClassName)){
				if(!is_subclass_of($sPluginClassName, $sBaseClassName)){
					throw new Exception('Plugin class does not extend plugin type class');
				}
			}else{
				throw new Exception('Plugin class does not exist');
			}

		}else{
			throw new Exception("Plugin class file '{$sPluginFile}' does not exist");
		}

		/**
		 * At least! Create plugin object and return it! 
		 */

		$oPlugin = new $sPluginClassName;
		return $oPlugin;
/*}}}*/
	}






	function getNextHeader(){}
}

?>
